/* ===========================================================
 * AFreeChart : a free chart library for Android(tm) platform.
 *              (based on JFreeChart and JCommon)
 * ===========================================================
 *
 * (C) Copyright 2010, by ICOMSYSTECH Co.,Ltd.
 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
 *
 * Project Info:
 *    AFreeChart: http://code.google.com/p/afreechart/
 *    JFreeChart: http://www.jfree.org/jfreechart/index.html
 *    JCommon   : http://www.jfree.org/jcommon/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Android is a trademark of Google Inc.]
 *
 * -----------------
 * ChartView.java
 * -----------------
 * (C) Copyright 2010, by ICOMSYSTECH Co.,Ltd.
 *
 * Original Author:  Niwano Masayoshi (for ICOMSYSTECH Co.,Ltd);
 * Contributor(s):   -;
 *
 * Changes
 * -------
 * 19-Nov-2010 : Version 0.0.1 (NM);
 * 14-Jan-2011 : renamed method name
 * 14-Jan-2011 : Updated API docs
 */

package eu.geopaparazzi.library.chart;

import java.util.ArrayList;
import java.util.Collection;
import java.util.EventListener;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.afree.chart.AFreeChart;
import org.afree.chart.ChartFactory;
import org.afree.chart.ChartRenderingInfo;
import org.afree.chart.ChartTouchEvent;
import org.afree.chart.ChartTouchListener;
import org.afree.chart.axis.NumberAxis;
import org.afree.chart.axis.ValueAxis;
import org.afree.chart.entity.ChartEntity;
import org.afree.chart.entity.EntityCollection;
import org.afree.chart.event.ChartChangeEvent;
import org.afree.chart.event.ChartChangeListener;
import org.afree.chart.event.ChartProgressEvent;
import org.afree.chart.event.ChartProgressListener;
import org.afree.chart.plot.Marker;
import org.afree.chart.plot.Movable;
import org.afree.chart.plot.Plot;
import org.afree.chart.plot.PlotOrientation;
import org.afree.chart.plot.PlotRenderingInfo;
import org.afree.chart.plot.XYPlot;
import org.afree.chart.plot.Zoomable;
import org.afree.chart.renderer.xy.XYItemRenderer;
import org.afree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.afree.data.xy.XYDataset;
import org.afree.data.xy.XYSeries;
import org.afree.data.xy.XYSeriesCollection;
import org.afree.graphics.SolidColor;
import org.afree.graphics.geom.Dimension;
import org.afree.graphics.geom.RectShape;
import org.afree.ui.Layer;
import org.afree.ui.RectangleInsets;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PointF;
import android.location.Location;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import eu.geopaparazzi.library.util.DynamicDoubleArray;
import eu.geopaparazzi.library.util.debug.Debug;
import eu.geopaparazzi.library.util.debug.Logger;

/**
 * A view that contains an updatable chart.
 * 
 * <p>The chart is created empty at first and can be populated 
 * through:</p>
 * <ul>
 *   <li>use of {@link XYChartView#createDatasetFromXY(double[], double[], String)} or 
 *       {@link XYChartView#createDatasetFromProfile(double[], double[], double[], String)}
 *       to create a dataset from XY values
 *       </li>
 *   <li>use of {@link XYChartView#setDataset(XYDataset, String, String, String)}
 *       to update the chart</li>
 * </ul>
 */
public class XYChartView extends View implements ChartChangeListener, ChartProgressListener {

    /** The user interface thread handler. */
    private Handler mHandler;

    public XYChartView( Context context ) {
        super(context);
        mHandler = new Handler();
        this.initialize();
    }

    public XYChartView( Context context, AttributeSet attrs ) {
        super(context, attrs);

        mHandler = new Handler();
        this.initialize();
    }

    /**
     * initialize parameters
     */
    private void initialize() {

        this.chartMotionListeners = new CopyOnWriteArrayList<ChartTouchListener>();
        this.info = new ChartRenderingInfo();
        this.minimumDrawWidth = DEFAULT_MINIMUM_DRAW_WIDTH;
        this.minimumDrawHeight = DEFAULT_MINIMUM_DRAW_HEIGHT;
        this.maximumDrawWidth = DEFAULT_MAXIMUM_DRAW_WIDTH;
        this.maximumDrawHeight = DEFAULT_MAXIMUM_DRAW_HEIGHT;
        this.moveTriggerDistance = DEFAULT_MOVE_TRIGGER_DISTANCE;
        new SolidColor(Color.BLUE);
        new SolidColor(Color.argb(0, 0, 255, 63));
        // new java.util.ArrayList();

        final AFreeChart chart = createChart(createEmptyDataset());

        setChart(chart);
    }

    /**
     * Create a dataset based on supplied XY data.
     * 
     * <p>
     * Note that this also sets the min and max values 
     * of the chart data, so the dataset created should
     * really be used through setDataset, so that the bounds 
     * are properly zoomed.
     * 
     * @param xArray the array of ordered X values.
     * @param yArray the array of ordered Y values.
     * @param seriesName the name to label the serie with.
     * @return the {@link XYSeriesCollection dataset}.
     */
    public XYSeriesCollection createDatasetFromXY( DynamicDoubleArray xArray, DynamicDoubleArray yArray, String seriesName ) {
        XYSeries xyS = new XYSeries(seriesName, true, true);

        int size = xArray.size();
        xMin = xArray.get(0);
        xMax = xArray.get(size - 1);
        yMin = Double.POSITIVE_INFINITY;
        yMax = Double.NEGATIVE_INFINITY;

        for( int i = 0; i < size; i++ ) {
            double y = yArray.get(i);
            yMin = Math.min(yMin, y);
            yMax = Math.max(yMax, y);
            xyS.add(xArray.get(i), y);
        }

        xMaxAll = xMax;
        xMinAll = xMin;
        yMaxAll = yMax;
        yMinAll = yMin;

        XYSeriesCollection xySC = new XYSeriesCollection();
        xySC.addSeries(xyS);
        return xySC;
    }

    /**
     * Create a dataset based on supplied data that are supposed to be coordinates and elevations for a profile view.
     * 
     * <p>
     * Note that this also sets the min and max values 
     * of the chart data, so the dataset created should
     * really be used through setDataset, so that the bounds 
     * are properly zoomed.
     * </p>
     * 
     * @param lonArray the array of longitudes.
     * @param latArray the array of latitudes.
     * @param elevArray the array of elevations.
     * @param seriesName the name to label the serie with.
     * @return the {@link XYSeriesCollection dataset}.
     */
    public XYSeriesCollection createDatasetFromProfile( DynamicDoubleArray lonArray, DynamicDoubleArray latArray,
            DynamicDoubleArray elevArray, String seriesName ) {
        XYSeries xyS = new XYSeries(seriesName, true, true);

        xMin = 0;
        xMax = 0;
        yMin = Double.POSITIVE_INFINITY;
        yMax = Double.NEGATIVE_INFINITY;

        double plat = 0;
        double plon = 0;
        double summedDistance = 0.0;
        for( int i = 0; i < lonArray.size(); i++ ) {
            double elev = elevArray.get(i);
            double lat = latArray.get(i);
            double lon = lonArray.get(i);

            double distance = 0.0;
            if (i > 0) {
                Location thisLoc = new Location("dummy1"); //$NON-NLS-1$
                thisLoc.setLongitude(lon);
                thisLoc.setLatitude(lat);
                Location thatLoc = new Location("dummy2"); //$NON-NLS-1$
                thatLoc.setLongitude(plon);
                thatLoc.setLatitude(plat);
                distance = thisLoc.distanceTo(thatLoc);
            }
            plat = lat;
            plon = lon;
            summedDistance = summedDistance + distance;

            yMin = Math.min(yMin, elev);
            yMax = Math.max(yMax, elev);

            xyS.add(summedDistance, elev);
        }
        xMax = summedDistance;

        xMaxAll = xMax;
        xMinAll = xMin;
        yMaxAll = yMax;
        yMinAll = yMin;

        XYSeriesCollection xySC = new XYSeriesCollection();
        xySC.addSeries(xyS);
        return xySC;
    }

    /**
     * Draw the given dataset on the chart.
     * 
     * @param xyDataset the dataset to draw.
     */
    public void setDataset( XYDataset xyDataset, String title, String xLabel, String yLabel ) {
        if (xyDataset == null) {
            xyDataset = createEmptyDataset();
        }

        chart.setTitle(title);
        XYPlot plot = (XYPlot) chart.getPlot();
        plot.setDataset(xyDataset);

        NumberAxis domainAxis = (NumberAxis) plot.getDomainAxis();
        domainAxis.setRange(xMin, xMax);
        domainAxis.setLabel(xLabel);
        ValueAxis valueAxis = plot.getRangeAxis();
        valueAxis.setRange(yMin, yMax);
        valueAxis.setLabel(yLabel);

        invalidate();
    }

    /**
     * Creates a chart, initially empty. Needs a call to createDataset and setDataset.
     *
     * @param dataset  a dataset.
     * @return a chart object.
     */
    @SuppressWarnings("nls")
    private AFreeChart createChart( XYDataset dataset ) {

        AFreeChart chart = ChartFactory.createXYLineChart("", "", "", dataset, // data
                PlotOrientation.VERTICAL, false, // create legend?
                true, // generate tooltips?
                false // generate URLs?
                );

        chart.setBackgroundPaintType(new SolidColor(Color.WHITE));

        XYPlot plot = (XYPlot) chart.getPlot();
        plot.setBackgroundPaintType(new SolidColor(Color.LTGRAY));
        plot.setDomainGridlinePaintType(new SolidColor(Color.WHITE));
        plot.setRangeGridlinePaintType(new SolidColor(Color.WHITE));
        plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
        // plot.setDomainCrosshairVisible(true);
        // plot.setRangeCrosshairVisible(true);

        XYItemRenderer r = plot.getRenderer();
        if (r instanceof XYLineAndShapeRenderer) {
            XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) r;
            renderer.setBaseShapesVisible(true);
            renderer.setBaseShapesFilled(true);
            renderer.setDrawSeriesLineAsPath(true);
        }

        NumberAxis axis = (NumberAxis) plot.getDomainAxis();
        axis.setRange(xMin, xMax);
        ValueAxis valueAxis = plot.getRangeAxis();
        valueAxis.setRange(yMin, yMax);
        // axis.setDateFormatOverride(new SimpleDateFormat("MMM-yyyy"));

        return chart;

    }

    /**
     * Creates an empty dataset.
     *
     * @return The dataset.
     */
    private XYDataset createEmptyDataset() {
        XYSeries xyS = new XYSeries("", true, true); //$NON-NLS-1$
        xyS.add(0, 0);
        xMin = -1;
        xMax = 1;
        yMin = -1;
        yMax = 1;

        XYSeriesCollection xySC = new XYSeriesCollection();
        xySC.addSeries(xyS);
        return xySC;
    }

    /**
     * Default setting for buffer usage.  The default has been changed to
     * <code>true</code> from version 1.0.13 onwards, because of a severe
     * performance problem with drawing the zoom RectShape using XOR (which
     * now happens only when the buffer is NOT used).
     */
    public static final boolean DEFAULT_BUFFER_USED = true;

    /** The default panel width. */
    public static final int DEFAULT_WIDTH = 680;

    /** The default panel height. */
    public static final int DEFAULT_HEIGHT = 420;

    /** The default limit below which chart scaling kicks in. */
    public static final int DEFAULT_MINIMUM_DRAW_WIDTH = 10;

    /** The default limit below which chart scaling kicks in. */
    public static final int DEFAULT_MINIMUM_DRAW_HEIGHT = 10;

    /** The default limit above which chart scaling kicks in. */
    public static final int DEFAULT_MAXIMUM_DRAW_WIDTH = 1280;

    /** The default limit above which chart scaling kicks in. */
    public static final int DEFAULT_MAXIMUM_DRAW_HEIGHT = 1000;

    /** The minimum size required to perform a zoom on a RectShape */
    public static final int DEFAULT_ZOOM_TRIGGER_DISTANCE = 10;

    /** The minimum size required to perform a move on a RectShape */
    public static final int DEFAULT_MOVE_TRIGGER_DISTANCE = 10;

    /** The chart that is displayed in the panel. */
    private AFreeChart chart;

    /** Storage for registered (chart) touch listeners. */
    private transient CopyOnWriteArrayList<ChartTouchListener> chartMotionListeners;

    /** The drawing info collected the last time the chart was drawn. */
    private ChartRenderingInfo info;

    /** The scale factor used to draw the chart. */
    private double scaleX;

    /** The scale factor used to draw the chart. */
    private double scaleY;

    /** The plot orientation. */
    private PlotOrientation orientation = PlotOrientation.VERTICAL;

    /**
     * The zoom RectShape starting point (selected by the user with touch).
     * This is a point on the screen, not the chart (which may have
     * been scaled up or down to fit the panel).
     */
    private PointF zoomPoint = null;

    /** Controls if the zoom RectShape is drawn as an outline or filled. */
    // private boolean fillZoomRectShape = true;

    private int moveTriggerDistance;

    /** The last touch position during panning. */
    // private Point panLast;

    private RectangleInsets insets = null;

    /**
     * The minimum width for drawing a chart (uses scaling for smaller widths).
     */
    private int minimumDrawWidth;

    /**
     * The minimum height for drawing a chart (uses scaling for smaller
     * heights).
     */
    private int minimumDrawHeight;

    /**
     * The maximum width for drawing a chart (uses scaling for bigger
     * widths).
     */
    private int maximumDrawWidth;

    /**
     * The maximum height for drawing a chart (uses scaling for bigger
     * heights).
     */
    private int maximumDrawHeight;

    private Dimension size = null;

    /** The chart anchor point. */
    private PointF anchor;

    /** A flag that controls whether or not domain moving is enabled. */
    private boolean domainMovable = false;

    /** A flag that controls whether or not range moving is enabled. */
    private boolean rangeMovable = false;

    private double accelX, accelY;
    private double friction = 0.8;
    private boolean inertialMovedFlag = false;
    private PointF lastTouch;

    private float mScale = 1.0f;

    private long mPrevTimeMillis = 0;
    private long mNowTimeMillis = System.currentTimeMillis();

    /**
     * touch event
     */
    public boolean onTouchEvent( MotionEvent ev ) {

        super.onTouchEvent(ev);
        int action = ev.getAction();
        int count = ev.getPointerCount();

        this.anchor = new PointF(ev.getX(), ev.getY());

        if (this.info != null) {
            EntityCollection entities = this.info.getEntityCollection();
            if (entities != null) {
            }
        }

        switch( action & MotionEvent.ACTION_MASK ) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_POINTER_DOWN:
            if (Debug.D)
                Logger.i(this, "ACTION_DOWN"); //$NON-NLS-1$
            if (count == 2 && this.multiTouchStartInfo == null) {
                setMultiTouchStartInfo(ev);
            } else if (count == 1 && this.singleTouchStartInfo == null) {
                setSingleTouchStartInfo(ev);
            }

            touched(ev);

            break;
        case MotionEvent.ACTION_MOVE:
            if (Debug.D)
                Logger.i(this, "ACTION_MOVE"); //$NON-NLS-1$
            if (count == 1 && this.singleTouchStartInfo != null) {
                moveAdjustment(ev);
            } else if (count == 2 && this.multiTouchStartInfo != null) {
                // scaleAdjustment(ev);
                zoomAdjustment(ev);
            }

            inertialMovedFlag = false;

            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_UP:
            if (Debug.D)
                Logger.i(this, "ACTION_UP"); //$NON-NLS-1$
            if (count <= 2) {
                this.multiTouchStartInfo = null;
                this.singleTouchStartInfo = null;
            }
            if (count <= 1) {
                this.singleTouchStartInfo = null;
            }

            // double click check
            if (count == 1) {
                mNowTimeMillis = System.currentTimeMillis();
                if (mNowTimeMillis - mPrevTimeMillis < 400) {
                    if (chart.getPlot() instanceof Movable) {
                        // restoreAutoBounds();
                        zoomToSelection(xMinAll, xMaxAll, yMinAll, yMaxAll);

                        mScale = 1.0f;
                        inertialMovedFlag = false;
                    }
                } else {
                    inertialMovedFlag = true;
                }
                mPrevTimeMillis = mNowTimeMillis;
            }
            break;
        default:
            break;
        }

        return true;
    }

    /**
     * MultiTouchStartInfo setting
     * @param ev
     */
    private void setMultiTouchStartInfo( MotionEvent ev ) {

        if (this.multiTouchStartInfo == null) {
            this.multiTouchStartInfo = new MultiTouchStartInfo();
        }

        // distance
        double distance = Math.sqrt(Math.pow(ev.getX(0) - ev.getX(1), 2) + Math.pow(ev.getY(0) - ev.getY(1), 2));
        this.multiTouchStartInfo.setDistance(distance);
    }

    /**
     * SingleTouchStartInfo setting
     * @param ev
     */
    private void setSingleTouchStartInfo( MotionEvent ev ) {

        if (this.singleTouchStartInfo == null) {
            this.singleTouchStartInfo = new SingleTouchStartInfo();
        }

        // start point
        this.singleTouchStartInfo.setX(ev.getX(0));
        this.singleTouchStartInfo.setY(ev.getY(0));
    }

    /**
     * Translate MotionEvent as TouchEvent
     * @param ev
     */
    private void moveAdjustment( MotionEvent ev ) {

        boolean hMove = false;
        boolean vMove = false;
        if (this.orientation == PlotOrientation.HORIZONTAL) {
            hMove = this.rangeMovable;
            vMove = this.domainMovable;
        } else {
            hMove = this.domainMovable;
            vMove = this.rangeMovable;
        }

        boolean moveTrigger1 = hMove && Math.abs(ev.getX(0) - this.singleTouchStartInfo.getX()) >= this.moveTriggerDistance;
        boolean moveTrigger2 = vMove && Math.abs(ev.getY(0) - this.singleTouchStartInfo.getY()) >= this.moveTriggerDistance;
        if (moveTrigger1 || moveTrigger2) {

            RectShape dataArea = this.info.getPlotInfo().getDataArea();

            double moveBoundX;
            double moveBoundY;
            double dataAreaWidth = dataArea.getWidth();
            double dataAreaHeight = dataArea.getHeight();

            // for touchReleased event, (horizontalZoom || verticalZoom)
            // will be true, so we can just test for either being false;
            // otherwise both are true

            if (!vMove) {
                moveBoundX = this.singleTouchStartInfo.getX() - ev.getX(0);
                moveBoundY = 0;
            } else if (!hMove) {
                moveBoundX = 0;
                moveBoundY = this.singleTouchStartInfo.getY() - ev.getY(0);
            } else {
                moveBoundX = this.singleTouchStartInfo.getX() - ev.getX(0);
                moveBoundY = this.singleTouchStartInfo.getY() - ev.getY(0);
            }
            accelX = moveBoundX;
            accelY = moveBoundY;

            lastTouch = new PointF(ev.getX(0), ev.getY(0));
            move(lastTouch, moveBoundX, moveBoundY, dataAreaWidth, dataAreaHeight);

        }

        setSingleTouchStartInfo(ev);
    }

    /**
     * 
     * @param moveBoundX
     * @param moveBoundY
     * @param dataAreaWidth
     * @param dataAreaHeight
     */
    private void move( PointF source, double moveBoundX, double moveBoundY, double dataAreaWidth, double dataAreaHeight ) {

        if (source == null) {
            throw new IllegalArgumentException("Null 'source' argument"); //$NON-NLS-1$
        }

        double hMovePercent = moveBoundX / dataAreaWidth;
        double vMovePercent = -moveBoundY / dataAreaHeight;

        Plot p = this.chart.getPlot();
        if (p instanceof Movable) {
            PlotRenderingInfo info = this.info.getPlotInfo();
            // here we tweak the notify flag on the plot so that only
            // one notification happens even though we update multiple
            // axes...
            // boolean savedNotify = p.isNotify();
            // p.setNotify(false);
            Movable z = (Movable) p;
            if (z.getOrientation() == PlotOrientation.HORIZONTAL) {
                z.moveDomainAxes(vMovePercent, info, source);
                z.moveRangeAxes(hMovePercent, info, source);
            } else {
                z.moveDomainAxes(hMovePercent, info, source);
                z.moveRangeAxes(vMovePercent, info, source);
            }
            // p.setNotify(savedNotify);

            // repaint
            invalidate();
        }

    }

    /**
     * Restores the auto-range calculation on both axes.
     */
    public void restoreAutoBounds() {
        Plot plot = this.chart.getPlot();
        if (plot == null) {
            return;
        }
        // here we tweak the notify flag on the plot so that only
        // one notification happens even though we update multiple
        // axes...
        // boolean savedNotify = plot.isNotify();
        // plot.setNotify(false);
        restoreAutoDomainBounds();
        restoreAutoRangeBounds();
        // plot.setNotify(savedNotify);
    }

    /**
     * Restores the auto-range calculation on the domain axis.
     */
    public void restoreAutoDomainBounds() {
        Plot plot = this.chart.getPlot();
        if (plot instanceof Zoomable) {
            Zoomable z = (Zoomable) plot;
            // here we tweak the notify flag on the plot so that only
            // one notification happens even though we update multiple
            // axes...
            // boolean savedNotify = plot.isNotify();
            // plot.setNotify(false);
            // we need to guard against this.zoomPoint being null
            PointF zp = (this.zoomPoint != null ? this.zoomPoint : new PointF());
            z.zoomDomainAxes(0.0, this.info.getPlotInfo(), zp);
            // plot.setNotify(savedNotify);
        }
    }

    /**
     * Restores the auto-range calculation on the range axis.
     */
    public void restoreAutoRangeBounds() {
        Plot plot = this.chart.getPlot();
        if (plot instanceof Zoomable) {
            Zoomable z = (Zoomable) plot;
            // here we tweak the notify flag on the plot so that only
            // one notification happens even though we update multiple
            // axes...
            // boolean savedNotify = plot.isNotify();
            // plot.setNotify(false);
            // we need to guard against this.zoomPoint being null
            PointF zp = (this.zoomPoint != null ? this.zoomPoint : new PointF());
            z.zoomRangeAxes(0.0, this.info.getPlotInfo(), zp);
            // plot.setNotify(savedNotify);
        }
    }

    protected void onSizeChanged( int w, int h, int oldw, int oldh ) {
        this.insets = new RectangleInsets(0, 0, 0, 0);
        this.size = new Dimension(w, h);
    }

    private RectangleInsets getInsets() {
        return this.insets;
    }

    /**
     * Returns the X scale factor for the chart.  This will be 1.0 if no
     * scaling has been used.
     *
     * @return The scale factor.
     */
    public double getScaleX() {
        return this.scaleX;
    }

    /**
     * Returns the Y scale factory for the chart.  This will be 1.0 if no
     * scaling has been used.
     *
     * @return The scale factor.
     */
    public double getScaleY() {
        return this.scaleY;
    }

    /**
     * Sets the chart that is displayed in the panel.
     *
     * @param chart  the chart (<code>null</code> permitted).
     */
    public void setChart( AFreeChart chart ) {

        // stop listening for changes to the existing chart
        if (this.chart != null) {
            this.chart.removeChangeListener(this);
            this.chart.removeProgressListener(this);
        }

        // add the new chart
        this.chart = chart;
        if (chart != null) {
            this.chart.addChangeListener(this);
            this.chart.addProgressListener(this);
            Plot plot = chart.getPlot();
            if (plot instanceof Zoomable) {
                Zoomable z = (Zoomable) plot;
                z.isRangeZoomable();
                this.orientation = z.getOrientation();
            }

            this.domainMovable = false;
            this.rangeMovable = false;
            if (plot instanceof Movable) {
                Movable z = (Movable) plot;
                this.domainMovable = z.isDomainMovable();
                this.rangeMovable = z.isRangeMovable();
                this.orientation = z.getOrientation();
            }
        } else {
            this.domainMovable = false;
            this.rangeMovable = false;
        }
        // if (this.useBuffer) {
        // this.refreshBuffer = true;
        // }
        repaint();

    }

    /**
     * Returns the minimum drawing width for charts.
     * <P>
     * If the width available on the panel is less than this, then the chart is
     * drawn at the minimum width then scaled down to fit.
     *
     * @return The minimum drawing width.
     */
    public int getMinimumDrawWidth() {
        return this.minimumDrawWidth;
    }

    /**
     * Sets the minimum drawing width for the chart on this panel.
     * <P>
     * At the time the chart is drawn on the panel, if the available width is
     * less than this amount, the chart will be drawn using the minimum width
     * then scaled down to fit the available space.
     *
     * @param width  The width.
     */
    public void setMinimumDrawWidth( int width ) {
        this.minimumDrawWidth = width;
    }

    /**
     * Returns the maximum drawing width for charts.
     * <P>
     * If the width available on the panel is greater than this, then the chart
     * is drawn at the maximum width then scaled up to fit.
     *
     * @return The maximum drawing width.
     */
    public int getMaximumDrawWidth() {
        return this.maximumDrawWidth;
    }

    /**
     * Sets the maximum drawing width for the chart on this panel.
     * <P>
     * At the time the chart is drawn on the panel, if the available width is
     * greater than this amount, the chart will be drawn using the maximum
     * width then scaled up to fit the available space.
     *
     * @param width  The width.
     */
    public void setMaximumDrawWidth( int width ) {
        this.maximumDrawWidth = width;
    }

    /**
     * Returns the minimum drawing height for charts.
     * <P>
     * If the height available on the panel is less than this, then the chart
     * is drawn at the minimum height then scaled down to fit.
     *
     * @return The minimum drawing height.
     */
    public int getMinimumDrawHeight() {
        return this.minimumDrawHeight;
    }

    /**
     * Sets the minimum drawing height for the chart on this panel.
     * <P>
     * At the time the chart is drawn on the panel, if the available height is
     * less than this amount, the chart will be drawn using the minimum height
     * then scaled down to fit the available space.
     *
     * @param height  The height.
     */
    public void setMinimumDrawHeight( int height ) {
        this.minimumDrawHeight = height;
    }

    /**
     * Returns the maximum drawing height for charts.
     * <P>
     * If the height available on the panel is greater than this, then the
     * chart is drawn at the maximum height then scaled up to fit.
     *
     * @return The maximum drawing height.
     */
    public int getMaximumDrawHeight() {
        return this.maximumDrawHeight;
    }

    /**
     * Sets the maximum drawing height for the chart on this panel.
     * <P>
     * At the time the chart is drawn on the panel, if the available height is
     * greater than this amount, the chart will be drawn using the maximum
     * height then scaled up to fit the available space.
     *
     * @param height  The height.
     */
    public void setMaximumDrawHeight( int height ) {
        this.maximumDrawHeight = height;
    }

    /**
     * Returns the chart rendering info from the most recent chart redraw.
     *
     * @return The chart rendering info.
     */
    public ChartRenderingInfo getChartRenderingInfo() {
        return this.info;
    }

    protected void onDraw( Canvas canvas ) {
        super.onDraw(canvas);

        // inertialMove();

        paintComponent(canvas);
    }

    @Override
    protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec ) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * Paints the component by drawing the chart to fill the entire component,
     * but allowing for the insets (which will be non-zero if a border has been
     * set for this component).  To increase performance (at the expense of
     * memory), an off-screen buffer image can be used.
     *
     * @param canvas  the graphics device for drawing on.
     */
    public void paintComponent( Canvas canvas ) {

        // first determine the size of the chart rendering area...
        Dimension size = getSize();
        RectangleInsets insets = getInsets();
        RectShape available = new RectShape(insets.getLeft(), insets.getTop(), size.getWidth() - insets.getLeft()
                - insets.getRight(), size.getHeight() - insets.getTop() - insets.getBottom());

        double drawWidth = available.getWidth();
        double drawHeight = available.getHeight();
        this.scaleX = 1.0;
        this.scaleY = 1.0;

        if (drawWidth < this.minimumDrawWidth) {
            this.scaleX = drawWidth / this.minimumDrawWidth;
            drawWidth = this.minimumDrawWidth;
        } else if (drawWidth > this.maximumDrawWidth) {
            this.scaleX = drawWidth / this.maximumDrawWidth;
            drawWidth = this.maximumDrawWidth;
        }

        if (drawHeight < this.minimumDrawHeight) {
            this.scaleY = drawHeight / this.minimumDrawHeight;
            drawHeight = this.minimumDrawHeight;
        } else if (drawHeight > this.maximumDrawHeight) {
            this.scaleY = drawHeight / this.maximumDrawHeight;
            drawHeight = this.maximumDrawHeight;
        }

        RectShape chartArea = new RectShape(0.0, 0.0, drawWidth, drawHeight);

        // are we using the chart buffer?
        // if (this.useBuffer) {
        //
        // // do we need to resize the buffer?
        // if ((this.chartBuffer == null)
        // || (this.chartBufferWidth != available.getWidth())
        // || (this.chartBufferHeight != available.getHeight())) {
        // this.chartBufferWidth = (int) available.getWidth();
        // this.chartBufferHeight = (int) available.getHeight();
        // GraphicsConfiguration gc = canvas.getDeviceConfiguration();
        // this.chartBuffer = gc.createCompatibleImage(
        // this.chartBufferWidth, this.chartBufferHeight,
        // Transparency.TRANSLUCENT);
        // this.refreshBuffer = true;
        // }
        //
        // // do we need to redraw the buffer?
        // if (this.refreshBuffer) {
        //
        // this.refreshBuffer = false; // clear the flag
        //
        // RectShape bufferArea = new RectShape(
        // 0, 0, this.chartBufferWidth, this.chartBufferHeight);
        //
        // Graphics2D bufferG2 = (Graphics2D)
        // this.chartBuffer.getGraphics();
        // RectShape r = new RectShape(0, 0, this.chartBufferWidth,
        // this.chartBufferHeight);
        // bufferG2.setPaint(getBackground());
        // bufferG2.fill(r);
        // if (scale) {
        // AffineTransform saved = bufferG2.getTransform();
        // AffineTransform st = AffineTransform.getScaleInstance(
        // this.scaleX, this.scaleY);
        // bufferG2.transform(st);
        // this.chart.draw(bufferG2, chartArea, this.anchor,
        // this.info);
        // bufferG2.setTransform(saved);
        // }
        // else {
        // this.chart.draw(bufferG2, bufferArea, this.anchor,
        // this.info);
        // }
        //
        // }
        //
        // // zap the buffer onto the panel...
        // canvas.drawImage(this.chartBuffer, insets.left, insets.top, this);
        //
        // }

        // TODO:AffineTransform
        // or redrawing the chart every time...
        // else {

        // AffineTransform saved = canvas.getTransform();
        // canvas.translate(insets.left, insets.top);
        // if (scale) {
        // AffineTransform st = AffineTransform.getScaleInstance(
        // this.scaleX, this.scaleY);
        // canvas.transform(st);
        // }
        // this.chart.draw(canvas, chartArea, this.anchor, this.info);
        // canvas.setTransform(saved);

        // }
        this.chart.draw(canvas, chartArea, this.anchor, this.info);

        // Iterator iterator = this.overlays.iterator();
        // while (iterator.hasNext()) {
        // Overlay overlay = (Overlay) iterator.next();
        // overlay.paintOverlay(canvas, this);
        // }

        // redraw the zoom RectShape (if present) - if useBuffer is false,
        // we use XOR so we can XOR the RectShape away again without redrawing
        // the chart
        // drawZoomRectShape(canvas, !this.useBuffer);

        // canvas.dispose();

        this.anchor = null;
        // this.verticalTraceLine = null;
        // this.horizontalTraceLine = null;

    }

    public Dimension getSize() {
        return this.size;
    }

    /**
     * Returns the anchor point.
     *
     * @return The anchor point (possibly <code>null</code>).
     */
    public PointF getAnchor() {
        return this.anchor;
    }

    public ChartRenderingInfo getInfo() {
        return info;
    }

    /**
     * Sets the anchor point.  This method is provided for the use of
     * subclasses, not end users.
     *
     * @param anchor  the anchor point (<code>null</code> permitted).
     */
    protected void setAnchor( PointF anchor ) {
        this.anchor = anchor;
    }

    /**
     * Information for multi touch start
     * @author ikeda
     *
     */
    private class MultiTouchStartInfo {
        private double distance = 0;

        public double getDistance() {
            return distance;
        }
        public void setDistance( double distance ) {
            this.distance = distance;
        }
    }

    private MultiTouchStartInfo multiTouchStartInfo = null;

    /**
     * Information for Single touch start
     * @author ikeda
     *
     */
    private class SingleTouchStartInfo {
        private double x = 0;
        private double y = 0;

        public double getX() {
            return x;
        }
        public void setX( double x ) {
            this.x = x;
        }
        public double getY() {
            return y;
        }
        public void setY( double y ) {
            this.y = y;
        }
    }

    private SingleTouchStartInfo singleTouchStartInfo = null;

    private double xMin;
    private double xMax;
    private double yMin;
    private double yMax;
    private double xMinAll;
    private double xMaxAll;
    private double yMinAll;
    private double yMaxAll;

    /**
     * Zoom 
     * @param ev
     */
    private void zoomAdjustment( MotionEvent ev ) {
        PointF point = new PointF((ev.getX(0) + ev.getX(1)) / 2, (ev.getY(0) + ev.getY(1)) / 2);
        // end distance
        double endDistance = Math.sqrt(Math.pow(ev.getX(0) - ev.getX(1), 2) + Math.pow(ev.getY(0) - ev.getY(1), 2));

        // zoom process
        zoom(point, this.multiTouchStartInfo.getDistance(), endDistance);

        // reset start point
        setMultiTouchStartInfo(ev);
    }

    /**
     * zoom
     * @param startDistance
     * @param endDistance
     */
    private void zoom( PointF source, double startDistance, double endDistance ) {

        Plot plot = this.chart.getPlot();
        PlotRenderingInfo info = this.info.getPlotInfo();

        if (plot instanceof Zoomable) {
            float scaleDistance = (float) (startDistance / endDistance);

            if (this.mScale * scaleDistance < 10.0f && this.mScale * scaleDistance > 0.1f) {
                this.mScale *= scaleDistance;
                Zoomable z = (Zoomable) plot;
                z.zoomDomainAxes(scaleDistance, info, source, false);
                z.zoomRangeAxes(scaleDistance, info, source, false);
            }
        }

        // repaint
        invalidate();
    }

    // inertialmove has been disabled to avoid infinite panning
    @SuppressWarnings("unused")
    private void inertialMove() {
        if (inertialMovedFlag == true) {
            RectShape dataArea = this.info.getPlotInfo().getDataArea();

            accelX *= friction;
            accelY *= friction;

            double dataAreaWidth = dataArea.getWidth();
            double dataAreaHeight = dataArea.getHeight();

            if (lastTouch != null) {
                move(lastTouch, accelX, accelY, dataAreaWidth, dataAreaHeight);
            }

            if (accelX < 0.1 && accelX > -0.1) {
                accelX = 0;
            }

            if (accelY < 0.1 && accelY > -0.1) {
                accelY = 0;
            }

            if (accelX == 0 && accelY == 0) {
                inertialMovedFlag = false;
            }
        }
    }
    /**
    * Receives notification of touch on the panel. These are
    * translated and passed on to any registered {@link ChartTouchListener}s.
    *
    * @param event  Information about the touch event.
    */
    public void touched( MotionEvent event ) {

        int x = (int) (event.getX() / this.scaleX);
        int y = (int) (event.getY() / this.scaleY);

        this.anchor = new PointF(x, y);
        if (this.chart == null) {
            return;
        }
        this.chart.setNotify(true); // force a redraw

        chart.handleClick((int) event.getX(), (int) event.getY(), info);
        inertialMovedFlag = false;

        // new entity code...
        if (this.chartMotionListeners.size() == 0) {
            return;
        }

        ChartEntity entity = null;
        if (this.info != null) {
            EntityCollection entities = this.info.getEntityCollection();
            if (entities != null) {
                entity = entities.getEntity(x, y);
            }
        }
        ChartTouchEvent chartEvent = new ChartTouchEvent(getChart(), event, entity);
        for( int i = chartMotionListeners.size() - 1; i >= 0; i-- ) {
            this.chartMotionListeners.get(i).chartTouched(chartEvent);
        }

    }

    /**
     * Returns the chart contained in the panel.
     *
     * @return The chart (possibly <code>null</code>).
     */
    public AFreeChart getChart() {
        return this.chart;
    }

    /**
     * Adds a listener to the list of objects listening for chart touch events.
     *
     * @param listener  the listener (<code>null</code> not permitted).
     */
    public void addChartTouchListener( ChartTouchListener listener ) {
        if (listener == null) {
            throw new IllegalArgumentException("Null 'listener' argument."); //$NON-NLS-1$
        }
        this.chartMotionListeners.add(listener);
    }

    /**
     * Removes a listener from the list of objects listening for chart touch
     * events.
     *
     * @param listener  the listener.
     */
    public void removeChartTouchListener( ChartTouchListener listener ) {
        this.chartMotionListeners.remove(listener);
    }

    /**
     * Returns an array of the listeners of the given type registered with the
     * panel.
     *
     * @param listenerType  the listener type.
     *
     * @return An array of listeners.
     */
    public EventListener[] getListeners() {
        return this.chartMotionListeners.toArray(new ChartTouchListener[0]);
    }

    /**
     * Schedule a user interface repaint.
     */
    public void repaint() {
        mHandler.post(new Runnable(){
            public void run() {
                invalidate();
            }
        });
    }

    /**
     * Receives notification of changes to the chart, and redraws the chart.
     *
     * @param event  details of the chart change event.
     */
    public void chartChanged( ChartChangeEvent event ) {
        // this.refreshBuffer = true;
        Plot plot = this.chart.getPlot();
        if (plot instanceof Zoomable) {
            Zoomable z = (Zoomable) plot;
            this.orientation = z.getOrientation();
        }
        repaint();
    }

    /**
     * Receives notification of a chart progress event.
     *
     * @param event  the event.
     */
    public void chartProgress( ChartProgressEvent event ) {
        // does nothing - override if necessary
    }

    public void clearMarkers() {
        Plot plot = chart.getPlot();
        if (plot instanceof XYPlot) {
            XYPlot xyPlot = (XYPlot) plot;
            Collection< ? > domainMarkers = xyPlot.getDomainMarkers(Layer.BACKGROUND);
            if (domainMarkers != null) {
                Iterator< ? > iterator = domainMarkers.iterator();
                // store the keys in a list first to escape a ConcurrentModificationException
                List<Marker> tmpMarkers = new ArrayList<Marker>();
                while( iterator.hasNext() ) {
                    Marker marker = (Marker) iterator.next();
                    tmpMarkers.add(marker);
                }
                // now remove them
                for( Marker marker : tmpMarkers ) {
                    xyPlot.removeDomainMarker(marker, Layer.BACKGROUND);
                }

                invalidate();
            }
        }
    }

    public void zoomToSelection( double xMin2, double xMax2, double yMin2, double yMax2 ) {
        XYPlot plot = (XYPlot) chart.getPlot();

        xMin = xMin2;
        xMax = xMax2;

        NumberAxis domainAxis = (NumberAxis) plot.getDomainAxis();
        domainAxis.setRange(xMin, xMax);

        if (!Double.isNaN(yMin2) && !Double.isNaN(yMax2)) {
            yMin = yMin2;
            yMax = yMax2;
            ValueAxis valueAxis = plot.getRangeAxis();
            valueAxis.setRange(yMin, yMax);
        }
        invalidate();
    }

    // public boolean onTouchEvent( MotionEvent ev ) {
    // super.onTouchEvent(ev);
    // int action = ev.getAction();
    // switch( action & MotionEvent.ACTION_MASK ) {
    // case MotionEvent.ACTION_DOWN:
    // case MotionEvent.ACTION_POINTER_DOWN:
    // case MotionEvent.ACTION_MOVE:
    // break;
    // case MotionEvent.ACTION_UP:
    // case MotionEvent.ACTION_POINTER_UP:
    // Plot plot = chart.getPlot();
    // if (plot instanceof XYPlot) {
    // double xValue = -1;
    // double yValue = -1;
    // XYPlot xyPlot = (XYPlot) plot;
    // int x = (int) (ev.getX() / this.scaleX);
    // int y = (int) (ev.getY() / this.scaleY);
    // PlotRenderingInfo plotInfo = info.getPlotInfo();
    // RectShape dataArea = plotInfo.getDataArea();
    // if (dataArea.contains(x, y)) {
    // ValueAxis xaxis = xyPlot.getDomainAxis();
    // if (xaxis != null) {
    // xValue = xaxis.java2DToValue(x, plotInfo.getDataArea(), xyPlot.getDomainAxisEdge());
    // }
    // ValueAxis yaxis = xyPlot.getRangeAxis();
    // if (yaxis != null) {
    // yValue = yaxis.java2DToValue(y, plotInfo.getDataArea(), xyPlot.getRangeAxisEdge());
    // }
    // }
    // final double fxValue = xValue;
    // final double fyValue = yValue;
    // new AsyncTask<String, Void, String>(){
    // protected String doInBackground( String... params ) {
    // return "";
    // }
    // @SuppressWarnings("nls")
    // protected void onPostExecute( String response ) { // on UI thread!
    // Log.i("TIMESERIDEMO", fxValue + "/" + fyValue);
    // Toast.makeText(getContext(), fxValue + "/" + fyValue, Toast.LENGTH_LONG);
    // }
    // }.execute((String) null);
    // }
    // break;
    // default:
    // break;
    // }
    // return true;
    // }
}
